#include <stdlib.h>
#include <stdio.h>
#include <timer.h>
#include <utypes.h>

#include <go32.h>
#include <sys/farptr.h>
#include <sys/nearptr.h>
#include <pc.h>
#include <ints.h>

#include "sound.h"

/** Tocando sons? */
static Bool sound_on = true;

/** Símbolos retornados pelo lexer
 */
enum LexerSym {
	NOTE_EOF, // End of File
	NOTE_C, // Dó
	NOTE_D, // Ré
	NOTE_E, // Mi
	NOTE_F, // Fá
	NOTE_G, // Sol
	NOTE_A, // Lá
	NOTE_B, // Si
	NOTE_PAUSE, // Pausa 
	NOTE_OPERANDS, //Operadores
	NOTE_FLAT, // Bemol
	NOTE_SHARP, // Sustenido
	NOTE_TEMPO, // Número de semínimas por minuto
	NOTE_INTERVAL, // Intervalo sem som entre as notas
	NOTE_OCTAVE, // Oitava
	NOTE_NUMBER // Número
};

/** \Lexer do ficheiro de músicas
 * \param fp Descritor do ficheiro
 * \return Símbolo léxico lido
 */
static enum LexerSym sound_lexer (FILE *fp) {
	while (1) {
		int c = getc (fp);
		
		switch (c) {
			case 'A': return NOTE_A;
			case 'B': return NOTE_B;
			case 'C': return NOTE_C;
			case 'D': return NOTE_D;
			case 'E': return NOTE_E;
			case 'F': return NOTE_F;
			case 'G': return NOTE_G;
			case '@': return NOTE_FLAT;
			case '#': return NOTE_SHARP;
			case '0': 
			case '1': 
			case '2': 
			case '3': 
			case '4': 
			case '5': 
			case '6': 
			case '7': 
			case '8': 
			case '9':
				ungetc (c, fp); // Deslê o caracter
				return NOTE_NUMBER;
			case 'P': return NOTE_PAUSE;
			case 'I': return NOTE_INTERVAL;
			case 'T': return NOTE_TEMPO;
			case 'O': return NOTE_OCTAVE;
			case -1: return NOTE_EOF;
		}
	}
}

/** French/Italian/Spanish/Portuguese notes names. 
 * 's' means sharp, e.g. Ds is D#
 * The central octave is the fourth, thus Do4 is the "central Do"
 */
enum NotesP { 						  SolsN1,LaN1,LasN1, SiN1,
	      Do0, Dos0, Re0, Res0, Mi0, Fa0, Fas0, Sol0, Sols0, La0, Las0, Si0,
	      Do1, Dos1, Re1, Res1, Mi1, Fa1, Fas1, Sol1, Sols1, La1, Las1, Si1,
	      Do2, Dos2, Re2, Res2, Mi2, Fa2, Fas2, Sol2, Sols2, La2, Las2, Si2,
	      Do3, Dos3, Re3, Res3, Mi3, Fa3, Fas3, Sol3, Sols3, La3, Las3, Si3,
	      Do4, Dos4, Re4, Res4, Mi4, Fa4, Fas4, Sol4, Sols4, La4, Las4, Si4,
	      Do5, Dos5, Re5, Res5, Mi5, Fa5, Fas5, Sol5, Sols5, La5, Las5, Si5,
	      Do6, Dos6, Re6, Res6, Mi6, Fa6, Fas6, Sol6, Sols6, La6, Las6, Si6,
	      Do7, Dos7, Re7, Res7, Mi7, Fa7, Fas7, Sol7, Sols7, La7, Las7, Si7,
	      Do8, Dos8, Re8, Res8};


/** Notes frequencies, in Hz, synchronized with arrays NotesN and NotesP.
 * E.G. frequency[Do5] is the frequency of a central Do, 261.63Hz
 *
 * NOTICE: this array is NOT defined in this header file,
 * as variables should not be declared in header files.
 * You have to copy/paste it to a source file (*.c).
 */
float notes_frequency[] = { 25.96/2, 27.5/2, 29.14/2, 30.87/2,
	16.35, 17.32, 18.35, 19.45, 20.6, 21.83, 23.12, 24.5, 25.96, 27.5, 29.14, 30.87,
	32.7, 34.65, 36.71, 38.89, 41.2, 43.65, 46.25, 49, 51.91, 55, 58.27, 61.74,
	65.41, 69.3, 73.42, 77.78, 82.41, 87.31, 92.5, 98, 103.83, 110, 116.54, 123.47,
	130.81, 138.59, 146.83, 155.56, 164.81, 174.61, 185, 196, 207.65, 220, 233.08, 246.94,
	261.63, 277.18, 293.66, 311.13, 329.63, 349.23, 369.99, 392, 415.3, 440, 466.16, 493.88,
	523.25, 554.37, 587.33, 622.25, 659.26, 698.46, 739.99, 783.99, 830.61, 880, 932.33, 987.77,
	1046.5, 1108.73, 1174.66, 1244.51, 1318.51, 1396.91, 1479.98, 1567.98, 1661.22, 1760, 1864.66, 1975.53,
	2093, 2217.46, 2349.32, 2489.02, 2637.02, 2793.83, 2959.96, 3135.96, 3322.44, 3520, 3729.31, 3951.07,
	4186.01, 4434.92, 4698.64, 4978.03};

/** Som */
struct Sound_ {
	float freq; /**< Frequência da nota. 0.0 se for uma pausa */
	int dur; /**< Duração em milisegundos */
	struct Sound_ *next; /**< Próxima nota */
};



Sound sound_load (const char *filename)
{
#define DUR_INDEX  	0	// Índice correspondente à duração
#define TEMPO_INDEX  	1	// Índice correspondente ao tempo
#define OCTAVE_INDEX  	2	// Oitava
#define INTERVAL_INDEX  3	// Intervalo entre as notas
#define MUSIC_NUM_INDEXES 4 	// Número de índices

	FILE *fp = fopen (filename, "r");
	
	if (fp == NULL) {
		/* Impossível abrir ficheiro */
		return NULL;
	}

	enum LexerSym sym; // Símbolo lido do ficheiro
	struct Sound_ *sound = NULL;
	struct Sound_* aux;
	int note = -1;
	int number [MUSIC_NUM_INDEXES]; // Número lido
	int index = DUR_INDEX; // Onde por o número lido
	int i;
	for (i = 0; i < MUSIC_NUM_INDEXES; i++)
		number[i] = 0;
	// 120 batidas por minuto
	number [TEMPO_INDEX] = 120;
	// 1 semínima toca
	number [DUR_INDEX] = 4;
	// 20 ms de intervalo entre as notas
	number [INTERVAL_INDEX] = 20;
	// Quarta oitava é a padrão
	number [OCTAVE_INDEX] = 4;

	// Lê o ficheiro
	do {
		sym = sound_lexer (fp);

		if (sym < NOTE_OPERANDS) { // Se é uma nota nova ou EOF
			aux = malloc (sizeof (struct Sound_));
			
			aux->next = sound;
			sound = aux;

			// Grava a nota anterior

			// Frequência
			if (note < 0) {
				sound->freq = 0.0;
			} else {
				// Limita a oitava da música
				if (number [OCTAVE_INDEX] > 7) 
					number [OCTAVE_INDEX] = 7;
				sound->freq = notes_frequency[note - 1 + 12 * number [OCTAVE_INDEX]];
			}
			// Duração: 
			//   tamanho_em_semifusas           60 * 1000
			// ------------------------ * ---------------------------   -  Intervalo_entre_as_notas
			//             4                numero_seminimas/minuto (TEMPO)
			//   ^^^^^^^^^^^                ^^^^^^^^^^^^^
			//  Tamanho em semínimas    *  Número de milisegundos / semínimas	
			sound->dur = number[DUR_INDEX] * 1000 * 60 / number[TEMPO_INDEX] / 4 - number[INTERVAL_INDEX];
			// Intervalo
			if (number[INTERVAL_INDEX] != 0) {
				aux = malloc (sizeof (struct Sound_));
				aux->freq = 0.0;
				aux->dur = number [INTERVAL_INDEX];
				aux->next = sound;
				sound = aux;
			}
			// Obtém a nota
			switch (sym) {
				case (NOTE_C):
					note = Do0 + 1;
					break;
				case (NOTE_D):
					note = Re0 + 1;
					break;
				case (NOTE_E):
					note = Mi0 + 1;
					break;
				case (NOTE_F):
					note = Fa0 + 1;
					break;
				case (NOTE_G):
					note = Sol0 + 1;
					break;
				case (NOTE_A):
					note = LaN1 + 1;
					break;
				case (NOTE_B):
					note = SiN1 + 1;
					break;
				case (NOTE_PAUSE):
					note = -1;
					break;
				default:

					break;
			}
			// Próximo número é a duração
			index = DUR_INDEX;
		} else if (sym == NOTE_NUMBER) {
			// Lê o número
			fscanf (fp, "%d", number + index);
			// Próximo número é a duração
			index = DUR_INDEX;
		} else if (sym == NOTE_FLAT) {
			note--; // desce 1 semitom
		} else if (sym == NOTE_SHARP) {
			note++; // Sobe 1 semitom
		} else if (sym == NOTE_TEMPO) {
			index = TEMPO_INDEX; // Próximo número a ser lido é o tempo
		} else if (sym == NOTE_INTERVAL) {
			index = INTERVAL_INDEX;
		} if (sym == NOTE_OCTAVE) {
			index = OCTAVE_INDEX;
		}

	} while (sym != NOTE_EOF);

	// Leu o ficheiro. Fecha ele
	fclose (fp);
	// As notas estão na ordem inversa. Reverte a lista
	aux = NULL;
	while (sound != NULL) {
		struct Sound_* aux2;

		aux2 = sound;
		sound = sound->next;
		aux2->next = aux;
		aux = aux2;
	}
	// O primeiro elemento não é tocado, portanto ele deve terminar imediatamente
	aux->freq = -1;
	aux->dur = 0;
	return aux;
}

/* Apaga um som */
void sound_delete (Sound sound)
{
	while (sound != NULL) {
		Sound aux = sound->next;
		free (sound);
		sound = aux;
	}
}

/** Antiga rotina de atendimento de interrupções */
static _go32_dpmi_seginfo old_t0_isr;

/** Efeito sonoro a ser tocado */
static Sound effect;

/** Música a ser tocada */
static Sound bg_music;

/** Música principal */
static Sound music_loop;

/** Rotina de atendimento de interrupções */
void sound_t0_isr (void);

/** Contador de milisegundos */
volatile unsigned int MSECS = 0;

/** Valor de msec quando a nota atual iniciou a ser tocada */
int last_msec;

/* Inicializa o sistema de sons */
void sound_init (void)
{
	// Inicializa o timer
	timer_init (0, LCOM_MODE);
	//timer_init (2, LCOM_MODE);
	timer_load (0, TIMER_CLK/1000.0);	
	install_asm_irq_handler (T0_IRQ, sound_t0_isr, &old_t0_isr); 
}

/* Encerra o sistema de sons */
void sound_finish (void)
{
	outportb (SPEAKER_CTRL, inportb(SPEAKER_CTRL) & ~SPEAKER_ENABLE);
	reinstall_asm_irq_handler (T0_IRQ, &old_t0_isr);
}

/* Toca uma música */
void sound_music_play (Sound music)
{
	bg_music = music;
	music_loop = music;
}

void sound_effect_play (Sound sound)
{
	effect = sound;
}

void sound_milisleep (int n) {
	unsigned int limit = MSECS + n;  // Quando terminamos

	//Bool play_note = false; // A nota deve ser tocada?
	Sound snd;

	while (MSECS < limit) {
		// Se não deve tocar nada
		if (!sound_on) {
			continue;
		}
		// Obtém o som sendo tocado
		snd = (effect != NULL ? effect : bg_music);
		// Se não há o que tocar
		if (snd == NULL) {
			// Loop da música
			snd = music_loop;
			continue;
		}

		if (snd->dur + last_msec < MSECS ) {
			// Fim da nota
			if (effect) {
				effect = snd->next;
			} else {
				bg_music = snd->next;
			}
			// Obtém o novo som
			snd = (effect != NULL ? effect : bg_music);
			if (snd != NULL) {
				fflush (stdout);
				if (snd->freq > 0) {
					timer_init (2, LCOM_MODE);
					timer_load (2, TIMER_CLK/snd->freq);
					outportb (SPEAKER_CTRL, inportb(SPEAKER_CTRL) | SPEAKER_ENABLE);
				} else {
					outportb (SPEAKER_CTRL, inportb(SPEAKER_CTRL) & ~SPEAKER_ENABLE);
				}
			}
			last_msec = MSECS;
		}
	}
}


void sound_set_msecs (unsigned int msecs)
{
	disable_irq (T0_IRQ);
	last_msec = last_msec + msecs - MSECS;
	MSECS = msecs;
	enable_irq (T0_IRQ);
}

void sound_toggle (void)
{
	sound_on = !sound_on;
}

